เจาะลึก Work Loop ของ React Scheduler และเรียนรู้เทคนิคการปรับแต่งที่ใช้งานได้จริงเพื่อเพิ่มประสิทธิภาพการทำงานของ Task ทำให้แอปพลิเคชันลื่นไหลและตอบสนองได้ดียิ่งขึ้น
การปรับแต่ง Work Loop ของ React Scheduler: เพิ่มประสิทธิภาพการทำงานของ Task ให้สูงสุด
Scheduler ของ React เป็นองค์ประกอบสำคัญที่จัดการและจัดลำดับความสำคัญของการอัปเดตเพื่อให้แน่ใจว่า UI จะราบรื่นและตอบสนองได้ดี การทำความเข้าใจวิธีการทำงานของ Work Loop ของ Scheduler และการใช้เทคนิคการปรับแต่งที่มีประสิทธิภาพเป็นสิ่งสำคัญอย่างยิ่งสำหรับการสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูง คู่มือฉบับสมบูรณ์นี้จะสำรวจ React Scheduler, Work Loop ของมัน และกลยุทธ์ในการเพิ่มประสิทธิภาพการทำงานของ Task ให้สูงสุด
ทำความเข้าใจ React Scheduler
React Scheduler หรือที่รู้จักกันในชื่อสถาปัตยกรรม Fiber เป็นกลไกเบื้องหลังของ React สำหรับการจัดการและจัดลำดับความสำคัญของการอัปเดต ก่อนที่จะมี Fiber, React ใช้กระบวนการ Reconciliation แบบซิงโครนัส ซึ่งอาจบล็อก main thread และทำให้ประสบการณ์ผู้ใช้สะดุด โดยเฉพาะสำหรับแอปพลิเคชันที่ซับซ้อน Scheduler ได้นำเสนอ Concurrency ซึ่งช่วยให้ React สามารถแบ่งงานเรนเดอร์ออกเป็นหน่วยเล็กๆ ที่สามารถหยุดชะงักได้
แนวคิดหลักของ React Scheduler ประกอบด้วย:
- Fiber: Fiber คือหน่วยของงาน (unit of work) แต่ละอินสแตนซ์ของ React component จะมี Fiber node ที่สอดคล้องกันซึ่งเก็บข้อมูลเกี่ยวกับ component, state ของมัน และความสัมพันธ์กับ component อื่นๆ ใน tree
- Work Loop: Work loop เป็นกลไกหลักที่วนซ้ำไปตาม Fiber tree, ทำการอัปเดต และเรนเดอร์การเปลี่ยนแปลงไปยัง DOM
- Prioritization: Scheduler จะจัดลำดับความสำคัญของการอัปเดตประเภทต่างๆ ตามความเร่งด่วน เพื่อให้แน่ใจว่า Task ที่มีความสำคัญสูง (เช่น การโต้ตอบของผู้ใช้) จะได้รับการประมวลผลอย่างรวดเร็ว
- Concurrency: React สามารถขัดจังหวะ, หยุดชั่วคราว หรือทำงานเรนเดอร์ต่อได้ ทำให้เบราว์เซอร์สามารถจัดการกับ Task อื่นๆ (เช่น การป้อนข้อมูลของผู้ใช้หรือแอนิเมชัน) ได้โดยไม่บล็อก main thread
เจาะลึก React Scheduler Work Loop
Work loop คือหัวใจของ React Scheduler มีหน้าที่ในการท่องไปตาม Fiber tree, ประมวลผลการอัปเดต และเรนเดอร์การเปลี่ยนแปลงไปยัง DOM การทำความเข้าใจวิธีการทำงานของ work loop เป็นสิ่งสำคัญสำหรับการระบุคอขวดของประสิทธิภาพที่อาจเกิดขึ้นและการนำกลยุทธ์การปรับแต่งไปใช้
เฟสของ Work Loop
Work loop ประกอบด้วยสองเฟสหลัก:
- Render Phase: ในเฟส render, React จะท่องไปตาม Fiber tree และกำหนดว่าต้องทำการเปลี่ยนแปลงอะไรบ้างกับ DOM เฟสนี้ยังเป็นที่รู้จักในชื่อเฟส "reconciliation"
- Begin Work: React เริ่มต้นที่ root Fiber node และท่องลงไปใน tree แบบเวียนเกิด (recursively) เปรียบเทียบ Fiber ปัจจุบันกับ Fiber ก่อนหน้า (ถ้ามี) กระบวนการนี้จะกำหนดว่า component จำเป็นต้องได้รับการอัปเดตหรือไม่
- Complete Work: เมื่อ React ท่องกลับขึ้นมาตาม tree มันจะคำนวณผลกระทบของการอัปเดตและเตรียมการเปลี่ยนแปลงที่จะนำไปใช้กับ DOM
- Commit Phase: ในเฟส commit, React จะนำการเปลี่ยนแปลงไปใช้กับ DOM และเรียกใช้ lifecycle methods
- Before Mutation: React เรียกใช้ lifecycle methods เช่น `getSnapshotBeforeUpdate`
- Mutation: React อัปเดต DOM nodes โดยการเพิ่ม, ลบ หรือแก้ไของค์ประกอบ
- Layout: React เรียกใช้ lifecycle methods เช่น `componentDidMount` และ `componentDidUpdate` และยังอัปเดต refs และจัดกำหนดการของ layout effects
เฟส render สามารถถูกขัดจังหวะโดย Scheduler ได้หากมี Task ที่มีความสำคัญสูงกว่าเข้ามา อย่างไรก็ตาม เฟส commit เป็นแบบซิงโครนัสและไม่สามารถถูกขัดจังหวะได้
การจัดลำดับความสำคัญและการจัดกำหนดการ
React ใช้อัลกอริทึมการจัดกำหนดการตามลำดับความสำคัญเพื่อกำหนดลำดับที่จะประมวลผลการอัปเดต การอัปเดตจะถูกกำหนดลำดับความสำคัญที่แตกต่างกันตามความเร่งด่วน
ระดับความสำคัญทั่วไป ได้แก่:
- Immediate Priority: ใช้สำหรับการอัปเดตเร่งด่วนที่ต้องประมวลผลทันที เช่น การป้อนข้อมูลของผู้ใช้ (เช่น การพิมพ์ในช่องข้อความ)
- User Blocking Priority: ใช้สำหรับการอัปเดตที่บล็อกการโต้ตอบของผู้ใช้ เช่น แอนิเมชันหรือทรานซิชัน
- Normal Priority: ใช้สำหรับการอัปเดตส่วนใหญ่ เช่น การเรนเดอร์เนื้อหาใหม่หรือการอัปเดตข้อมูล
- Low Priority: ใช้สำหรับการอัปเดตที่ไม่สำคัญ เช่น background tasks หรือ analytics
- Idle Priority: ใช้สำหรับการอัปเดตที่สามารถเลื่อนออกไปได้จนกว่าเบราว์เซอร์จะว่าง เช่น การดึงข้อมูลล่วงหน้า (pre-fetching) หรือการคำนวณที่ซับซ้อน
React ใช้ `requestIdleCallback` API (หรือ polyfill) เพื่อจัดกำหนดการ Task ที่มีความสำคัญต่ำ ทำให้เบราว์เซอร์สามารถปรับปรุงประสิทธิภาพและหลีกเลี่ยงการบล็อก main thread ได้
เทคนิคการปรับแต่งเพื่อการทำงานของ Task ที่มีประสิทธิภาพ
การปรับแต่ง work loop ของ React Scheduler เกี่ยวข้องกับการลดปริมาณงานที่ต้องทำในระหว่างเฟส render และทำให้แน่ใจว่าการอัปเดตได้รับการจัดลำดับความสำคัญอย่างถูกต้อง ต่อไปนี้คือเทคนิคหลายอย่างเพื่อปรับปรุงประสิทธิภาพการทำงานของ Task:
1. Memoization
Memoization เป็นเทคนิคการปรับแต่งที่มีประสิทธิภาพซึ่งเกี่ยวข้องกับการแคชผลลัพธ์ของการเรียกใช้ฟังก์ชันที่มีค่าใช้จ่ายสูงและส่งคืนผลลัพธ์ที่แคชไว้เมื่อมีการป้อนข้อมูลเดิมอีกครั้ง ใน React, memoization สามารถนำไปใช้ได้ทั้งกับ components และ values
`React.memo`
`React.memo` เป็น higher-order component ที่ทำ memoize ให้กับ functional component มันจะป้องกันไม่ให้ component re-render หาก props ของมันไม่มีการเปลี่ยนแปลง โดยค่าเริ่มต้น `React.memo` จะทำการเปรียบเทียบ props แบบตื้นๆ (shallow comparison) คุณยังสามารถส่งฟังก์ชันเปรียบเทียบที่กำหนดเองเป็นอาร์กิวเมนต์ที่สองของ `React.memo` ได้
ตัวอย่าง:
import React from 'react';
const MyComponent = React.memo(function MyComponent(props) {
// โลจิกของ Component
return (
<div>
{props.value}
</div>
);
});
export default MyComponent;
`useMemo`
`useMemo` เป็น hook ที่ทำ memoize ให้กับค่า มันรับฟังก์ชันที่คำนวณค่าและ dependency array ฟังก์ชันจะถูกเรียกใช้ใหม่เมื่อ dependency ตัวใดตัวหนึ่งมีการเปลี่ยนแปลงเท่านั้น สิ่งนี้มีประโยชน์สำหรับการทำ memoize การคำนวณที่มีค่าใช้จ่ายสูงหรือการสร้าง references ที่เสถียร
ตัวอย่าง:
import React, { useMemo } from 'react';
function MyComponent(props) {
const expensiveValue = useMemo(() => {
// ทำการคำนวณที่มีค่าใช้จ่ายสูง
return computeExpensiveValue(props.data);
}, [props.data]);
return (
<div>
{expensiveValue}
</div>
);
}
`useCallback`
`useCallback` เป็น hook ที่ทำ memoize ให้กับฟังก์ชัน มันรับฟังก์ชันและ dependency array ฟังก์ชันจะถูกสร้างขึ้นใหม่เมื่อ dependency ตัวใดตัวหนึ่งมีการเปลี่ยนแปลงเท่านั้น สิ่งนี้มีประโยชน์สำหรับการส่ง callbacks ไปยัง child components ที่ใช้ `React.memo`
ตัวอย่าง:
import React, { useCallback } from 'react';
function MyComponent(props) {
const handleClick = useCallback(() => {
// จัดการ click event
console.log('Clicked!');
}, []);
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
2. Virtualization
Virtualization (หรือที่เรียกว่า windowing) เป็นเทคนิคสำหรับการเรนเดอร์รายการหรือตารางขนาดใหญ่อย่างมีประสิทธิภาพ แทนที่จะเรนเดอร์ทุกรายการในครั้งเดียว virtualization จะเรนเดอร์เฉพาะรายการที่มองเห็นได้ใน viewport เท่านั้น เมื่อผู้ใช้เลื่อน รายการใหม่จะถูกเรนเดอร์และรายการเก่าจะถูกลบออกไป
มีไลบรารีหลายตัวที่ให้บริการ virtualization components สำหรับ React ได้แก่:
- `react-window`: ไลบรารีขนาดเล็กสำหรับการเรนเดอร์รายการและตารางขนาดใหญ่
- `react-virtualized`: ไลบรารีที่ครอบคลุมกว่าซึ่งมี virtualization components ที่หลากหลาย
ตัวอย่างการใช้ `react-window`:
import React from 'react';
import { FixedSizeList } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>
Row {index}
</div>
);
function MyListComponent(props) {
return (
<FixedSizeList
height={400}
width={300}
itemSize={30}
itemCount={props.items.length}
>
{Row}
</FixedSizeList>
);
}
3. Code Splitting
Code splitting เป็นเทคนิคสำหรับการแบ่งแอปพลิเคชันของคุณออกเป็นส่วนเล็กๆ (chunks) ที่สามารถโหลดได้ตามต้องการ ซึ่งจะช่วยลดเวลาในการโหลดเริ่มต้นและปรับปรุงประสิทธิภาพโดยรวมของแอปพลิเคชันของคุณ
React มีหลายวิธีในการทำ code splitting:
- `React.lazy` และ `Suspense`: `React.lazy` ช่วยให้คุณสามารถ import components แบบไดนามิกได้ และ `Suspense` ช่วยให้คุณสามารถแสดง UI สำรองในขณะที่ component กำลังโหลด
- Dynamic Imports: คุณสามารถใช้ dynamic imports (`import()`) เพื่อโหลดโมดูลตามต้องการได้
ตัวอย่างการใช้ `React.lazy` และ `Suspense`:
import React, { lazy, Suspense } from 'react';
const MyComponent = lazy(() => import('./MyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}
4. Debouncing และ Throttling
Debouncing และ throttling เป็นเทคนิคในการจำกัดอัตราการเรียกใช้ฟังก์ชัน ซึ่งจะมีประโยชน์ในการปรับปรุงประสิทธิภาพของ event handlers ที่ถูกเรียกใช้บ่อยๆ เช่น scroll events หรือ resize events
- Debouncing: Debouncing จะหน่วงการทำงานของฟังก์ชันจนกว่าจะพ้นช่วงเวลาที่กำหนดหลังจากที่ฟังก์ชันถูกเรียกใช้ครั้งล่าสุด
- Throttling: Throttling จะจำกัดอัตราการทำงานของฟังก์ชัน โดยฟังก์ชันจะทำงานเพียงครั้งเดียวภายในช่วงเวลาที่กำหนด
ตัวอย่างการใช้ไลบรารี `lodash` สำหรับ debouncing:
import React, { useState, useEffect } from 'react';
import { debounce } from 'lodash';
function MyComponent() {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
};
const debouncedHandleChange = debounce(handleChange, 300);
useEffect(() => {
return () => {
debouncedHandleChange.cancel();
};
}, [debouncedHandleChange]);
return (
<input type="text" onChange={debouncedHandleChange} />
);
}
5. การหลีกเลี่ยงการ Re-render ที่ไม่จำเป็น
หนึ่งในสาเหตุที่พบบ่อยที่สุดของปัญหาด้านประสิทธิภาพในแอปพลิเคชัน React คือการ re-render ที่ไม่จำเป็น มีกลยุทธ์หลายอย่างที่สามารถช่วยลดการ re-render ที่ไม่จำเป็นเหล่านี้ได้:
- Immutable Data Structures: การใช้โครงสร้างข้อมูลที่ไม่สามารถเปลี่ยนแปลงได้ (immutable) ทำให้แน่ใจได้ว่าการเปลี่ยนแปลงข้อมูลจะสร้างออบเจ็กต์ใหม่แทนที่จะแก้ไขออบเจ็กต์ที่มีอยู่ ซึ่งช่วยให้ตรวจจับการเปลี่ยนแปลงและป้องกันการ re-render ที่ไม่จำเป็นได้ง่ายขึ้น ไลบรารีเช่น Immutable.js และ Immer สามารถช่วยในเรื่องนี้ได้
- Pure Components: Class components สามารถขยาย `React.PureComponent` ซึ่งจะทำการเปรียบเทียบ props และ state แบบตื้นๆ (shallow comparison) ก่อนที่จะ re-render ซึ่งคล้ายกับ `React.memo` สำหรับ functional components
- Properly Keyed Lists: เมื่อเรนเดอร์รายการต่างๆ ตรวจสอบให้แน่ใจว่าแต่ละรายการมี key ที่ไม่ซ้ำกันและเสถียร ซึ่งช่วยให้ React อัปเดตรายการได้อย่างมีประสิทธิภาพเมื่อมีการเพิ่ม, ลบ หรือจัดลำดับรายการใหม่
- Avoiding Inline Functions and Objects as Props: การสร้างฟังก์ชันหรือออบเจ็กต์ใหม่แบบ inline ภายในเมธอด render ของ component จะทำให้ child components re-render แม้ว่าข้อมูลจะไม่มีการเปลี่ยนแปลงก็ตาม ควรใช้ `useCallback` และ `useMemo` เพื่อหลีกเลี่ยงปัญหานี้
6. การจัดการ Event อย่างมีประสิทธิภาพ
ปรับแต่งการจัดการ event โดยลดงานที่ทำภายใน event handlers ให้เหลือน้อยที่สุด หลีกเลี่ยงการคำนวณที่ซับซ้อนหรือการจัดการ DOM โดยตรงภายใน event handlers แต่ควรเลื่อนงานเหล่านี้ไปเป็นการทำงานแบบ asynchronous หรือใช้ web workers สำหรับงานที่ต้องใช้การคำนวณสูง
7. การทำโปรไฟล์และการตรวจสอบประสิทธิภาพ
ทำโปรไฟล์แอปพลิเคชัน React ของคุณเป็นประจำเพื่อระบุคอขวดของประสิทธิภาพและส่วนที่ต้องปรับปรุง React DevTools มีความสามารถในการทำโปรไฟล์ที่มีประสิทธิภาพซึ่งช่วยให้คุณสามารถตรวจสอบเวลาในการเรนเดอร์ของ component, ระบุการ re-render ที่ไม่จำเป็น และวิเคราะห์ call stack ได้ ใช้เครื่องมือตรวจสอบประสิทธิภาพเพื่อติดตามเมตริกประสิทธิภาพที่สำคัญใน production และระบุปัญหาที่อาจเกิดขึ้นก่อนที่จะส่งผลกระทบต่อผู้ใช้
ตัวอย่างการใช้งานจริงและกรณีศึกษา
ลองพิจารณาตัวอย่างการใช้งานจริงบางส่วนเกี่ยวกับวิธีการนำเทคนิคการปรับแต่งเหล่านี้ไปใช้:
- รายการสินค้า E-commerce: เว็บไซต์ E-commerce ที่แสดงรายการสินค้าจำนวนมากจะได้รับประโยชน์จาก virtualization เพื่อปรับปรุงประสิทธิภาพการเลื่อน การทำ memoize ให้กับ product components ยังสามารถป้องกันการ re-render ที่ไม่จำเป็นเมื่อมีการเปลี่ยนแปลงเฉพาะจำนวนหรือสถานะของรถเข็น
- Interactive Dashboard: แดชบอร์ดที่มีแผนภูมิและวิดเจ็ตแบบโต้ตอบหลายรายการสามารถใช้ code splitting เพื่อโหลดเฉพาะ components ที่จำเป็นตามต้องการ การทำ debouncing ให้กับ user input events สามารถป้องกันการอัปเดตที่มากเกินไปและปรับปรุงการตอบสนองได้
- ฟีดโซเชียลมีเดีย: ฟีดโซเชียลมีเดียที่แสดงโพสต์จำนวนมากสามารถใช้ virtualization เพื่อเรนเดอร์เฉพาะโพสต์ที่มองเห็นได้ การทำ memoize ให้กับ post components และการปรับแต่งการโหลดรูปภาพสามารถเพิ่มประสิทธิภาพได้อีก
สรุป
การปรับแต่ง work loop ของ React Scheduler เป็นสิ่งจำเป็นสำหรับการสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพสูง ด้วยการทำความเข้าใจวิธีการทำงานของ Scheduler และการใช้เทคนิคต่างๆ เช่น memoization, virtualization, code splitting, debouncing และกลยุทธ์การเรนเดอร์อย่างรอบคอบ คุณจะสามารถปรับปรุงประสิทธิภาพการทำงานของ Task ได้อย่างมาก และสร้างประสบการณ์ผู้ใช้ที่ราบรื่นและตอบสนองได้ดียิ่งขึ้น อย่าลืมทำโปรไฟล์แอปพลิเคชันของคุณเป็นประจำเพื่อระบุคอขวดของประสิทธิภาพและปรับปรุงกลยุทธ์การปรับแต่งของคุณอย่างต่อเนื่อง
ด้วยการนำแนวทางปฏิบัติที่ดีที่สุดเหล่านี้ไปใช้ นักพัฒนาสามารถสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพและประสิทธิผลมากขึ้น ซึ่งมอบประสบการณ์ผู้ใช้ที่ดีขึ้นในอุปกรณ์และสภาวะเครือข่ายที่หลากหลาย และท้ายที่สุดจะนำไปสู่การมีส่วนร่วมและความพึงพอใจของผู้ใช้ที่เพิ่มขึ้น